/*
 * Copyright (c) 2011-2012 Johann Hanne
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to
 * deal in the Software without restriction, including without limitation the
 * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
 * sell copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
 * IN THE SOFTWARE.
 */

#include <stdio.h>
#include <string.h>
#include <termios.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>

#include <linux/serial.h>
#include <sys/ioctl.h>

#include "serport.h"

serport_t serport_open(const char *port) {
    int fd;
    struct termios tio;

    fd = open(port, O_RDWR);
    if (fd < 0) {
        return -1;
    }

    if (tcgetattr(fd, &tio) == 0) {
        /* Disable all the serial terminal processing; we just want to send and
           receive 8-bit-clean binary data */
        cfmakeraw(&tio);

        /* make read() return as fast as possible (this depends on the
           non-canonical mode which is set by cfmakeraw) */
        tio.c_cc[VMIN] = 0;
        tio.c_cc[VTIME] = 0;

        tcsetattr(fd, TCSANOW, &tio);
    }

    return fd;
}

void serport_close(serport_t hdl) {
    close(hdl);
}

/*
 * Example mode: "115200 8N1"
 */
int serport_setmode(serport_t fd, const char *mode) {
    struct termios tio;
    unsigned long int speed;
    speed_t ospeed;
    int nonstandardspeed = 0;
    char *p;
    struct serial_struct serial_struct;

    speed = strtoul(mode, &p, 10);
    if (p == NULL || *p != ' ') {
        return -1;
    }

    switch (speed) {
    case 0:
        ospeed = B0;
        break;
    case 50:
        ospeed = B50;
        break;
    case 75:
        ospeed = B75;
        break;
    case 110:
        ospeed = B110;
        break;
    case 134:
        ospeed = B134;
        break;
    case 150:
        ospeed = B150;
        break;
    case 200:
        ospeed = B200;
        break;
    case 300:
        ospeed = B300;
        break;
    case 600:
        ospeed = B600;
        break;
    case 1200:
        ospeed = B1200;
        break;
    case 1800:
        ospeed = B1800;
        break;
    case 2400:
        ospeed = B2400;
        break;
    case 4800:
        ospeed = B4800;
        break;
    case 9600:
        ospeed = B9600;
        break;
    case 19200:
        ospeed = B19200;
        break;
    case 38400:
        ospeed = B38400;
        break;
    case 57600:
        ospeed = B57600;
        break;
    case 115200:
        ospeed = B115200;
        break;
    default:
        ospeed = B38400;
        nonstandardspeed = 1;
        break;
    }

    if (tcgetattr(fd, &tio) < 0) {
        return -1;
    }

    cfsetspeed(&tio, ospeed);

    p++;
    switch (*p) {
    case '5':
        tio.c_cflag = (tio.c_cflag & ~CSIZE) | CS5;
        break;
    case '6':
        tio.c_cflag = (tio.c_cflag & ~CSIZE) | CS6;
        break;
    case '7':
        tio.c_cflag = (tio.c_cflag & ~CSIZE) | CS7;
        break;
    case '8':
        tio.c_cflag = (tio.c_cflag & ~CSIZE) | CS8;
        break;
    default:
        return -1;
    }

    p++;
    switch (*p) {
    case 'N':
        tio.c_cflag &= ~PARENB;
        break;
    case 'O':
        tio.c_cflag = (tio.c_cflag & ~CMSPAR) | (PARENB | PARODD);
        break;
    case 'E':
        tio.c_cflag = (tio.c_cflag & ~(CMSPAR | PARODD)) | PARENB;
        break;
    case 'M':
        tio.c_cflag = tio.c_cflag | (CMSPAR | PARENB | PARODD);
        break;
    case 'S':
        tio.c_cflag = (tio.c_cflag & ~PARODD) | (PARENB | CMSPAR);
        break;
    default:
        return -1;
    }

    p++;
    if (strcmp(p, "1") == 0) {
        tio.c_cflag &= ~CSTOPB;
    } else if (strcmp(p, "2") == 0) {
        tio.c_cflag |= CSTOPB;
    } else {
        return -1;
    }

    if (tcsetattr(fd, TCSANOW, &tio) < 0) {
        return -1;
    }

    if (ioctl(fd, TIOCGSERIAL, &serial_struct) != 0) {
        return -1;
    }
    if (nonstandardspeed) {
        serial_struct.custom_divisor = (serial_struct.baud_base + speed / 2) / speed;
        serial_struct.flags = (serial_struct.flags & ~ASYNC_SPD_MASK) | ASYNC_SPD_CUST;
    } else {
        serial_struct.flags &= ~ASYNC_SPD_MASK;
    }
    if (ioctl(fd, TIOCSSERIAL, &serial_struct) != 0) {
        return -1;
    }

    return 0;
}

int serport_getmode(serport_t fd, char *mode, size_t mode_bufsize) {
    struct termios tio;
    speed_t ispeed, ospeed;
    unsigned long int speed;
    int bytesize;
    char parity;
    int stopbits;
    struct serial_struct serial_struct;

    if (tcgetattr(fd, &tio) < 0) {
      return -1;
    }

    ispeed = cfgetispeed(&tio);
    ospeed = cfgetospeed(&tio);
    if (ispeed != ospeed) {
        return -1;
    }

    if (ioctl(fd, TIOCGSERIAL, &serial_struct) != 0) {
        return -1;
    }
    if ((serial_struct.flags & ASYNC_SPD_CUST) != 0) {
        speed = serial_struct.baud_base / serial_struct.custom_divisor;
    } else {
        switch (ospeed) {
        case B0:
            speed = 0;
            break;
        case B50:
            speed = 50;
            break;
        case B75:
            speed = 75;
            break;
        case B110:
            speed = 110;
            break;
        case B134:
            speed = 134;
            break;
        case B150:
            speed = 150;
            break;
        case B200:
            speed = 200;
            break;
        case B300:
            speed = 300;
            break;
        case B600:
            speed = 600;
            break;
        case B1200:
            speed = 1200;
            break;
        case B1800:
            speed = 1800;
            break;
        case B2400:
            speed = 2400;
            break;
        case B4800:
            speed = 4800;
            break;
        case B9600:
            speed = 9600;
            break;
        case B19200:
            speed = 19200;
            break;
        case B38400:
            speed = 38400;
            break;
        case B57600:
            speed = 57600;
            break;
        case B115200:
            speed = 115200;
            break;
        case B230400:
            speed = 230400;
            break;
        case B460800:
            speed = 460800;
            break;
        default:
            return -1;
        }
    }

    switch (tio.c_cflag & CSIZE) {
    case CS5:
        bytesize = 5;
        break;
    case CS6:
        bytesize = 6;
        break;
    case CS7:
        bytesize = 7;
        break;
    case CS8:
        bytesize = 8;
        break;
    default:
        return -1;
    }

    if ((tio.c_cflag & PARENB) == 0) {
        parity = 'N';
    } else if ((tio.c_cflag & CMSPAR) != 0) {
        if ((tio.c_cflag & PARODD) != 0) {
            parity = 'M';
        } else {
            parity = 'S';
        }
    } else {
        if ((tio.c_cflag & PARODD) != 0) {
            parity = 'O';
        } else {
            parity = 'E';
        }
    }

    if ((tio.c_cflag & CSTOPB) != 0) {
        stopbits = 2;
    } else {
        stopbits = 1;
    }

    snprintf(mode, mode_bufsize, "%lu %d%c%d", speed, bytesize, parity, stopbits);

    return 0;
}

ssize_t serport_write(serport_t fd, const uint8_t *buf, size_t count) {
    return write(fd, buf, count);
}

ssize_t serport_read(serport_t fd, uint8_t *buf, size_t count) {
    return read(fd, buf, count);
}

